<?php

/**
 * SigmaCMS - Content Management System
 *
 * Copyright (C) 2008 Pavol Biely
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 *
 * @package    SigmaCMS
 * @author     Pavol Biely <pavolbiely@gmail.com>
 * @copyright  2008 Pavol Biely
 * @license    http://www.gnu.org/licenses/gpl.txt   GNU General Public License
 * @link       http://pabi3.com/
 * @since      0.1.0
 */

final class Parser
{
	/**
	 * Object of the class Driver
	 *
	 * @var object
	 */
	private $_driver;

	/**
	 * Variables defined by user
	 *
	 * @var array
	 */
	private $_vars = array();

	/**
	 * Stop parsing
	 *
	 * @var boolean
	 */
	private $_halt = false;

	/**
	 * Constructor
	 *
	 * @return void
	 */
	public function __construct()
	{
		// create an instance of Driver class
		$this->_driver = new Driver;
	}

	/**
	 * Instance of the Parser class
	 *
	 * @var object
	 */
	static private $_instance = NULL;

	/**
	 * Singleton - creates a unique object
	 *
	 * @return object
	 */
	static public function singleton()
	{
		if (self::$_instance == NULL) {
			self::$_instance = new Parser;
		}
		return self::$_instance;
	}

	/**
	 * Return an instance of the Driver class
	 *
	 * @return object
	 */
	public function getDriver()
	{
		return $this->_driver;
	}

	/**
	 * Create a new Template
	 *
	 * @param object $ins
	 * @return void
	 */
	public function setTemplate(Template $ins)
	{
		$this->_driver->template[] = $ins;
	}

	/**
	 * Remove the last used Template
	 *
	 * @return void
	 */
	public function unsetTemplate()
	{
		array_pop($this->_driver->template);
	}

	/**
	 * Enable/Disable parsing keywords
	 *
	 * @return void|boolean
	 */
	public function halt($state = NULL)
	{
		if ($state === NULL) {
			return $this->_halt;
		}
		$this->_halt = (boolean) $state;
	}

	/**
	 * Add a new keyword
	 *
	 * @param string $key
	 * @param string $value
	 * @return void
	 */
	public function addKeyword($key, $value = NULL, $removePrevious = true)
	{
		// remove the previous keyword if this have the same name?
		if ($removePrevious) {
			$this->removeKeyword($key);
		}

		if (!array_key_exists($key,$this->_vars)) {
			// set the keyword value
			$this->_vars[$key] = $value;
		}
	}

	/**
	 * Remove a keyword
	 *
	 * @return void
	 */
	public function removeKeyword($key)
	{
		unset($this->_vars[$key]);
	}

	/**
	 * Analyze a keyword
	 *
	 * @param string $keyword
	 * @return void
	 */
	private function parseKeyword($keyword)
	{
	    // functions must begin with a letter or number
		if (preg_match('/^[a-zA-Z0-9]+/',$keyword)) {
			// divide function from parameters
			$func = preg_split('/\(|\)$/', $keyword, -1, PREG_SPLIT_NO_EMPTY);

			// method name
			$method = 'parse_'.$func[0];

			// if the parser is halted, skip parsing keywords
			if ($this->_halt === true && !in_array($func[0], array('if', 'elseif', 'else', 'endif'))) {
				return;
			}

			// function without parameters
			if (count($func) == 1) {
				// call the method
				@call_user_func(array($this->_driver,$method));
			} else if (count($func) > 1) { // function with parameters
				// all parameters are split by commas, exclude those which are encapsulated by apostrophe
				$params = $this->parseParameters($func[1]);

				// does any parameter refer to a defined variable?
				for ($j = 0; $j < count($params); $j++) {
					$matches = array();
					if (preg_match('/^&([a-zA-Z0-9]+)$/',$params[$j],$matches)) {
						$var = substr($params[$j],1);
						if (array_key_exists($var,$this->_vars)) {
							$params[$j] = is_bool($this->_vars[$var]) ? (integer) $this->_vars[$var] : $this->_vars[$var];
						}
					}
				}

				// call the method if it exists
				if (method_exists($this->_driver, $method)) {
					@call_user_func_array(array($this->_driver, $method), $params);
				} else {
					// add a plugin name as the first parameter of this keyword (it will represent the plugin ID)
					array_unshift($params,$func[0]);

					// call the plugin
					@call_user_func_array(array($this->_driver, 'parse_plugin'), $params);
				}
			}
		}

		// save value to a variable
		else if (preg_match('/^&([a-zA-Z0-9]+)=(.*)$/',$keyword,$matches)) {
			// uložiť hodnotu do poľa
			if ($this->_halt !== true) $this->_vars[$matches[1]] = $matches[2];
		}

		// reference to a variable by ampersand character (&)
		else if (preg_match('/^&([a-zA-Z0-9]+)$/',$keyword,$matches)) {
			// does the array key exist?
			if ($this->_halt !== true && array_key_exists($matches[1],$this->_vars)) {
				// print keyword value
				echo $this->_vars[$matches[1]];
			}
		}
	}

	/**
	 * Get parameters of keyword
	 *
	 * @param string $inputParams
	 * @return array
	 */
	private function parseParameters($inputParams = NULL)
	{
		// function must have input parameters
		if (!$inputParams) {
			return array();
		}

		// found parameters
		$params = array();
		
		// opened parameter
		$open   = false;
		
		// opened parameter by apostroph
		$apos   = false;
		
		// algorithm
		for ($i = 0; $i < strlen($inputParams); $i++) {
			switch ($inputParams{$i}) {
				case "'":
					// escaped apostrophs
					if ($inputParams{$i-1} == "\\") {
						$params[count($params)-1] .= "'";
						break;
					}
					if ($open) {
						$open = false;
						$apos = false;
					} else {
						$open     = true;
						$apos     = true;
						$params[] = NULL;
					}
					break;
				case ",":
					// if the parameter was opened by an apostroph, the comma is unimportant here and it will be normally printed
					if ($apos) {
						$params[count($params)-1] .= ",";
						break;
					}
					if ($open) {
						$open = false;
					} else {
						$params[] = NULL;
					}
					break;
				default:
					if (count($params) == 0) {
						$params[] = $inputParams{$i};
					} else {
						$params[count($params)-1] .= $inputParams{$i};
					}
					break;
			}
		}

		// remove escape characters
		foreach ($params as $key => &$value) {
			$value = trim($value);
			if (strlen($value) > 0) {
				$value = stripslashes($value);
			} else {
				unset($value);
			}
		}

		return $params;
	}

	/**
	 * Analyze code
	 *
	 * @param string $input
	 * @return void
	 */
	public function analyze($input = NULL)
	{
		// is the driver an object?
		if (!is_object($this->_driver)) return;

		// split keywords
		$input = preg_split('/<%|%>/', $input);

		// algorithm for resolution keywords from a normal text
		for ($i = 0; $i < count($input); $i++) {
			if ($i%2 == 0) {
				// skip every empty string
				if (empty($input[$i]) || strlen(trim($input[$i])) == 0) {
					continue;
				}
				// print a text
				echo $input[$i];
			} else {
				// parse a keyword
				$this->parseKeyword($input[$i]);
			}
		}
	}
}
